From: Owen W. Taylor Date: Fri, 5 Mar 2010 22:20:56 +0000 (-0500) Subject: Fix size negotiation for StScrollView X-Git-Tag: archive/raspbian/50.5-1+rpi1~1^2~1013^2^2~10332 X-Git-Url: https://dgit.raspbian.org/%22http://www.example.com/cgi/%22/%22http:/www.example.com/cgi/%22?a=commitdiff_plain;h=ffd25fe9e495ac1148108c02d6d6aa87fd5283b7;p=gnome-shell.git Fix size negotiation for StScrollView StScrollable: Document how size negotation now works between the parent and scrollable child. StBoxLayout: Adapt to the new contract for how size negotiation works; in particular, handle being allocated less than the minimum size when scrolled and treat the minimum size as the size of the scrolled area in instead of the natural size. StScrollView: Substantially rewrite with fixes including: - Implement new size negotation contract; this allows us to determine scrollbar visibility without having to connect to the adjustment. - Implement all ALWAYS along with the existing NEVER/AUTO - When hiding and showing scrollbars and shadows, don't hide and show widgets, just turn on and off including them in pick and paint. This avoids queueing relayouts. - Cleanups for the code for connecting to adjustments, for changing policy, and for turning on and off shadows. scroll-view-sizing.js: New test case for StScrollView, allowing resizing the scroll view interactively, changing the scrollbar policies and turning shadows on and off. https://bugzilla.gnome.org/show_bug.cgi?id=611740 --- diff --git a/src/st/st-box-layout.c b/src/st/st-box-layout.c index 08f304234..45aed4be9 100644 --- a/src/st/st-box-layout.c +++ b/src/st/st-box-layout.c @@ -569,11 +569,25 @@ st_box_layout_get_preferred_height (ClutterActor *actor, gfloat *min_height_p, gfloat *natural_height_p) { + StBoxLayout *self = ST_BOX_LAYOUT (actor); + StBoxLayoutPrivate *priv = self->priv; StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); st_theme_node_adjust_for_width (theme_node, &for_width); - get_content_preferred_height (ST_BOX_LAYOUT (actor), for_width, + if (priv->hadjustment) + { + /* If we're scrolled, the parent calls us with the width that + * we'll actually get, which can be smaller than the minimum + * width that we give our contents. + */ + gfloat min_width; + + get_content_preferred_width (self, -1, &min_width, NULL); + for_width = MAX (for_width, min_width); + } + + get_content_preferred_height (self, for_width, min_height_p, natural_height_p); st_theme_node_adjust_preferred_height (theme_node, @@ -743,10 +757,11 @@ st_box_layout_allocate (ClutterActor *actor, avail_width = content_box.x2 - content_box.x1; avail_height = content_box.y2 - content_box.y1; - get_content_preferred_height (ST_BOX_LAYOUT (actor), avail_width, - &min_height, &natural_height); get_content_preferred_width (ST_BOX_LAYOUT (actor), avail_height, &min_width, &natural_width); + get_content_preferred_height (ST_BOX_LAYOUT (actor), MAX (avail_width, min_width), + &min_height, &natural_height); + /* update adjustments for scrolling */ if (priv->vadjustment) @@ -755,7 +770,7 @@ st_box_layout_allocate (ClutterActor *actor, g_object_set (G_OBJECT (priv->vadjustment), "lower", 0.0, - "upper", natural_height, + "upper", min_height, "page-size", avail_height, "step-increment", avail_height / 6, "page-increment", avail_height, @@ -771,7 +786,7 @@ st_box_layout_allocate (ClutterActor *actor, g_object_set (G_OBJECT (priv->hadjustment), "lower", 0.0, - "upper", natural_width, + "upper", min_width, "page-size", avail_width, "step-increment", avail_width / 6, "page-increment", avail_width, @@ -781,6 +796,18 @@ st_box_layout_allocate (ClutterActor *actor, st_adjustment_set_value (priv->hadjustment, prev_value); } + if (avail_height < min_height) + { + avail_height = min_height; + content_box.y2 = content_box.y1 + avail_height; + } + + if (avail_width < min_width) + { + avail_width = min_width; + content_box.x2 = content_box.x1 + avail_width; + } + if (priv->is_vertical) { expand_amount = MAX (0, avail_height - natural_height); @@ -894,10 +921,7 @@ st_box_layout_allocate (ClutterActor *actor, child_box.y1 = (int)(0.5 + position); child_box.y2 = (int)(0.5 + next_position); child_box.x1 = content_box.x1; - if (priv->hadjustment) - child_box.x2 = content_box.x1 + MAX (avail_width, natural_width); - else - child_box.x2 = content_box.x2; + child_box.x2 = content_box.x2; _st_allocate_fill (ST_WIDGET (actor), child, &child_box, xalign, yalign, xfill, yfill); @@ -918,10 +942,7 @@ st_box_layout_allocate (ClutterActor *actor, } child_box.y1 = content_box.y1; - if (priv->vadjustment) - child_box.y2 = content_box.y1 + MAX (avail_height, natural_height); - else - child_box.y2 = content_box.y2; + child_box.y2 = content_box.y2; _st_allocate_fill (ST_WIDGET (actor), child, &child_box, xalign, yalign, xfill, yfill); diff --git a/src/st/st-scroll-view.c b/src/st/st-scroll-view.c index 51263fd82..8407e2979 100644 --- a/src/st/st-scroll-view.c +++ b/src/st/st-scroll-view.c @@ -32,6 +32,35 @@ * allow the user to move around the scrollable area. */ +/* TODO: The code here currently only deals with height-for-width + * allocation; width-for-height allocation would need a second set of + * code paths through get_preferred_height()/get_preferred_width()/allocate() + * that reverse the roles of the horizontal and vertical scrollbars. + * + * TODO: The multiple layout passes with and without scrollbars when + * using the automatic policy causes considerable inefficiency because + * it breaks request caching; we should saved the last size passed + * into allocate() and if it's the same as previous size not repeat + * the determination of scrollbar visibility. This requires overriding + * queue_relayout() so we know when to discard the saved value. + * + * The size negotiation between the #StScrollView and the child is + * described in the documentation for #StScrollable; the significant + * part to note there is that reported minimum sizes for a scrolled + * child are the minimum sizes when no scrollbar is needed. This allows + * us to determine what scrollbars are visible without a need to look + * inside the #StAdjustment. + * + * The second simplification that we make that allows us to implement + * a straighforward height-for-width negotiation without multiple + * allocate passes is that when the vertical scrollbar policy is + * AUTO, we always reserve space for the vertical scrollbar in the + * reported minimum and natural size. + * + * See https://bugzilla.gnome.org/show_bug.cgi?id=611740 for a more + * detailed description of the considerations involved. + */ + #include "st-scroll-view.h" #include "st-marshal.h" #include "st-scroll-bar.h" @@ -65,8 +94,11 @@ struct _StScrollViewPrivate ClutterActor *hscroll; ClutterActor *vscroll; - GtkPolicyType hscroll_policy; - GtkPolicyType vscroll_policy; + StAdjustment *hadjustment; + StAdjustment *vadjustment; + + GtkPolicyType hscrollbar_policy; + GtkPolicyType vscrollbar_policy; ClutterActor *top_shadow; ClutterActor *bottom_shadow; @@ -78,6 +110,10 @@ struct _StScrollViewPrivate gboolean row_size_set : 1; gboolean column_size_set : 1; guint mouse_scroll : 1; + guint hscrollbar_visible : 1; + guint vscrollbar_visible : 1; + guint top_shadow_visible : 1; + guint bottom_shadow_visible : 1; }; enum { @@ -108,10 +144,10 @@ st_scroll_view_get_property (GObject *object, g_value_set_object (value, priv->vscroll); break; case PROP_HSCROLLBAR_POLICY: - g_value_set_enum (value, priv->hscroll_policy); + g_value_set_enum (value, priv->hscrollbar_policy); break; case PROP_VSCROLLBAR_POLICY: - g_value_set_enum (value, priv->vscroll_policy); + g_value_set_enum (value, priv->vscrollbar_policy); break; case PROP_MOUSE_SCROLL: g_value_set_boolean (value, priv->mouse_scroll); @@ -125,26 +161,24 @@ st_scroll_view_get_property (GObject *object, } static void -update_shadow_visibility (GObject *gobject, - GParamSpec *arg1, - gpointer user_data) +update_shadow_visibility (StScrollView *scroll) { - StAdjustment *vadjust = ST_ADJUSTMENT (gobject); - StScrollViewPrivate *priv = ST_SCROLL_VIEW (user_data)->priv; - - gdouble value, lower, upper, page_size; + StScrollViewPrivate *priv = scroll->priv; - st_adjustment_get_values (vadjust, &value, &lower, &upper, NULL, NULL, &page_size); + if (priv->vshadows && priv->vadjustment) + { + gdouble value, lower, upper, page_size; - if (fabs (value - lower) > 0.1 && priv->vshadows) - clutter_actor_show (priv->top_shadow); - else - clutter_actor_hide (priv->top_shadow); + st_adjustment_get_values (priv->vadjustment, &value, &lower, &upper, NULL, NULL, &page_size); - if (fabs (upper - value - page_size) > 0.1 && priv->vshadows) - clutter_actor_show (priv->bottom_shadow); + priv->top_shadow_visible = fabs (value - lower) > 0.1; + priv->bottom_shadow_visible = fabs (upper - value - page_size) > 0.1; + } else - clutter_actor_hide (priv->bottom_shadow); + { + priv->top_shadow_visible = FALSE; + priv->bottom_shadow_visible = FALSE; + } } /** @@ -159,7 +193,6 @@ void st_scroll_view_set_vshadows (StScrollView *self, gboolean vshadows) { - StAdjustment *vadjust; StScrollViewPrivate *priv = ST_SCROLL_VIEW (self)->priv; vshadows = vshadows != FALSE; @@ -168,12 +201,30 @@ st_scroll_view_set_vshadows (StScrollView *self, priv->vshadows = vshadows; - vadjust = st_scroll_bar_get_adjustment (ST_SCROLL_BAR(priv->vscroll)); + if (vshadows) + { + if (priv->top_shadow) + { + clutter_actor_show (priv->top_shadow); + clutter_actor_show (priv->bottom_shadow); + } + else + { + priv->top_shadow = g_object_new (ST_TYPE_BIN, "style-class", "top-shadow", NULL); + priv->bottom_shadow = g_object_new (ST_TYPE_BIN, "style-class", "bottom-shadow", NULL); - if (!vadjust) - return; + clutter_actor_set_parent (priv->bottom_shadow, CLUTTER_ACTOR (self)); + clutter_actor_set_parent (priv->top_shadow, CLUTTER_ACTOR (self)); + } + } + else + { + clutter_actor_hide (priv->top_shadow); + clutter_actor_hide (priv->bottom_shadow); + } + + update_shadow_visibility (self); - update_shadow_visibility (G_OBJECT (vadjust), NULL, self); g_object_notify (G_OBJECT (self), "vshadows"); } @@ -198,11 +249,11 @@ st_scroll_view_set_property (GObject *object, case PROP_HSCROLLBAR_POLICY: st_scroll_view_set_policy (self, g_value_get_enum (value), - priv->vscroll_policy); + priv->vscrollbar_policy); break; case PROP_VSCROLLBAR_POLICY: st_scroll_view_set_policy (self, - priv->hscroll_policy, + priv->hscrollbar_policy, g_value_get_enum (value)); break; default: @@ -247,14 +298,14 @@ st_scroll_view_paint (ClutterActor *actor) CLUTTER_ACTOR_CLASS (st_scroll_view_parent_class)->paint (actor); /* paint our custom children */ - if (CLUTTER_ACTOR_IS_VISIBLE (priv->hscroll)) + if (priv->hscrollbar_visible && CLUTTER_ACTOR_IS_VISIBLE (priv->hscroll)) clutter_actor_paint (priv->hscroll); - if (CLUTTER_ACTOR_IS_VISIBLE (priv->vscroll)) + if (priv->vscrollbar_visible && CLUTTER_ACTOR_IS_VISIBLE (priv->vscroll)) clutter_actor_paint (priv->vscroll); - if (CLUTTER_ACTOR_IS_VISIBLE (priv->top_shadow)) + if (priv->top_shadow_visible) clutter_actor_paint (priv->top_shadow); - if (CLUTTER_ACTOR_IS_VISIBLE (priv->bottom_shadow)) + if (priv->bottom_shadow_visible) clutter_actor_paint (priv->bottom_shadow); } @@ -268,37 +319,46 @@ st_scroll_view_pick (ClutterActor *actor, CLUTTER_ACTOR_CLASS (st_scroll_view_parent_class)->pick (actor, color); /* paint our custom children */ - if (CLUTTER_ACTOR_IS_VISIBLE (priv->hscroll)) + if (priv->hscrollbar_visible && CLUTTER_ACTOR_IS_VISIBLE (priv->hscroll)) clutter_actor_paint (priv->hscroll); - if (CLUTTER_ACTOR_IS_VISIBLE (priv->vscroll)) + if (priv->vscrollbar_visible && CLUTTER_ACTOR_IS_VISIBLE (priv->vscroll)) clutter_actor_paint (priv->vscroll); - - if (CLUTTER_ACTOR_IS_VISIBLE (priv->top_shadow)) - clutter_actor_paint (priv->top_shadow); - if (CLUTTER_ACTOR_IS_VISIBLE (priv->bottom_shadow)) - clutter_actor_paint (priv->bottom_shadow); } static double -get_scrollbar_width (StScrollView *scroll_view) +get_scrollbar_width (StScrollView *scroll) { - StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (scroll_view)); - double result = DEFAULT_SCROLLBAR_WIDTH; + StScrollViewPrivate *priv = scroll->priv; + + if (CLUTTER_ACTOR_IS_VISIBLE (priv->vscroll)) + { + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (scroll)); + double result = DEFAULT_SCROLLBAR_WIDTH; - st_theme_node_get_length (theme_node, "scrollbar-width", FALSE, &result); + st_theme_node_get_length (theme_node, "scrollbar-width", FALSE, &result); - return result; + return result; + } + else + return 0; } static double -get_scrollbar_height (StScrollView *scroll_view) +get_scrollbar_height (StScrollView *scroll) { - StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (scroll_view)); - double result = DEFAULT_SCROLLBAR_WIDTH; + StScrollViewPrivate *priv = scroll->priv; + + if (CLUTTER_ACTOR_IS_VISIBLE (priv->hscroll)) + { + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (scroll)); + double result = DEFAULT_SCROLLBAR_WIDTH; - st_theme_node_get_length (theme_node, "scrollbar-height", FALSE, &result); + st_theme_node_get_length (theme_node, "scrollbar-height", FALSE, &result); - return result; + return result; + } + else + return 0; } static void @@ -309,32 +369,64 @@ st_scroll_view_get_preferred_width (ClutterActor *actor, { StScrollViewPrivate *priv = ST_SCROLL_VIEW (actor)->priv; StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + gboolean account_for_vscrollbar = FALSE; + gfloat min_width = 0, natural_width; + gfloat child_min_width, child_natural_width; if (!priv->child) return; st_theme_node_adjust_for_height (theme_node, &for_height); - /* Our natural width is the natural width of the child */ - clutter_actor_get_preferred_width (priv->child, - for_height, - NULL, - natural_width_p); + clutter_actor_get_preferred_width (priv->child, -1, + &child_min_width, &child_natural_width); + + natural_width = child_natural_width; + + switch (priv->hscrollbar_policy) + { + case GTK_POLICY_NEVER: + min_width = child_min_width; + break; + case GTK_POLICY_ALWAYS: + case GTK_POLICY_AUTOMATIC: + /* Should theoretically use the min width of the hscrollbar, + * but that's not cleanly defined at the moment */ + min_width = 0; + break; + } + + switch (priv->vscrollbar_policy) + { + case GTK_POLICY_NEVER: + account_for_vscrollbar = FALSE; + break; + case GTK_POLICY_ALWAYS: + account_for_vscrollbar = TRUE; + break; + case GTK_POLICY_AUTOMATIC: + /* For automatic scrollbars, we always request space for the vertical + * scrollbar; we won't know whether we actually need one until our + * height is assigned in allocate(). + */ + account_for_vscrollbar = TRUE; + break; + } - /* Add space for the scroll-bar if we can determine it will be necessary */ - if ((for_height >= 0) && natural_width_p) + account_for_vscrollbar = priv->vscrollbar_policy != GTK_POLICY_NEVER; + if (account_for_vscrollbar) { - gfloat natural_height; + float sb_width = get_scrollbar_width (ST_SCROLL_VIEW (actor)); - clutter_actor_get_preferred_height (priv->child, -1.0, - NULL, - &natural_height); - if (for_height < natural_height) - *natural_width_p += get_scrollbar_width (ST_SCROLL_VIEW (actor)); + min_width += sb_width; + natural_width += sb_width; } if (min_width_p) - *min_width_p = 0; + *min_width_p = min_width; + + if (natural_width_p) + *natural_width_p = natural_width; st_theme_node_adjust_preferred_width (theme_node, min_width_p, natural_width_p); } @@ -347,32 +439,82 @@ st_scroll_view_get_preferred_height (ClutterActor *actor, { StScrollViewPrivate *priv = ST_SCROLL_VIEW (actor)->priv; StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + gboolean account_for_hscrollbar = FALSE; + gfloat min_height = 0, natural_height; + gfloat child_min_height, child_natural_height; + gfloat child_min_width; + gfloat sb_width; if (!priv->child) return; st_theme_node_adjust_for_width (theme_node, &for_width); - /* Our natural height is the natural height of the child */ - clutter_actor_get_preferred_height (priv->child, - for_width, - NULL, - natural_height_p); + clutter_actor_get_preferred_width (priv->child, -1, + &child_min_width, NULL); + + natural_height = child_natural_height; + + if (min_height_p) + *min_height_p = 0; + + sb_width = get_scrollbar_width (ST_SCROLL_VIEW (actor)); + + switch (priv->vscrollbar_policy) + { + case GTK_POLICY_NEVER: + break; + case GTK_POLICY_ALWAYS: + case GTK_POLICY_AUTOMATIC: + /* We've requested space for the scrollbar, subtract it back out */ + for_width -= sb_width; + break; + } + + switch (priv->hscrollbar_policy) + { + case GTK_POLICY_NEVER: + account_for_hscrollbar = FALSE; + break; + case GTK_POLICY_ALWAYS: + account_for_hscrollbar = TRUE; + break; + case GTK_POLICY_AUTOMATIC: + account_for_hscrollbar = for_width < child_min_width; + break; + } + + clutter_actor_get_preferred_height (priv->child, for_width, + &child_min_height, &child_natural_height); + + natural_height = child_natural_height; + + switch (priv->vscrollbar_policy) + { + case GTK_POLICY_NEVER: + min_height = child_min_height; + break; + case GTK_POLICY_ALWAYS: + case GTK_POLICY_AUTOMATIC: + /* Should theoretically use the min height of the hscrollbar, + * but that's not cleanly defined at the moment */ + min_height = 0; + break; + } - /* Add space for the scroll-bar if we can determine it will be necessary */ - if ((for_width >= 0) && natural_height_p) + if (account_for_hscrollbar) { - gfloat natural_width; + float sb_height = get_scrollbar_height (ST_SCROLL_VIEW (actor)); - clutter_actor_get_preferred_width (priv->child, -1.0, - NULL, - &natural_width); - if (for_width < natural_width) - *natural_height_p += get_scrollbar_height (ST_SCROLL_VIEW (actor)); + min_height += sb_height; + natural_height += sb_height; } if (min_height_p) - *min_height_p = 0; + *min_height_p = min_height; + + if (natural_height_p) + *natural_height_p = natural_height; st_theme_node_adjust_preferred_height (theme_node, min_height_p, natural_height_p); } @@ -385,6 +527,7 @@ st_scroll_view_allocate (ClutterActor *actor, ClutterActorBox content_box, child_box; ClutterActorClass *parent_parent_class; gfloat avail_width, avail_height, sb_width, sb_height; + gboolean hscrollbar_visible, vscrollbar_visible; StScrollViewPrivate *priv = ST_SCROLL_VIEW (actor)->priv; StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); @@ -408,13 +551,73 @@ st_scroll_view_allocate (ClutterActor *actor, avail_height = content_box.y2 - content_box.y1; sb_width = get_scrollbar_width (ST_SCROLL_VIEW (actor)); - sb_height = get_scrollbar_width (ST_SCROLL_VIEW (actor)); + sb_height = get_scrollbar_height (ST_SCROLL_VIEW (actor)); - if (!CLUTTER_ACTOR_IS_VISIBLE (priv->vscroll)) - sb_width = 0; + /* Determine what scrollbars are visible. The basic idea of the handling + * of an automatic scrollbars is that we start off with the assumption + * that that we don't need any scrollbars, see if that works, and if + * not add horizontal and vertical scrollbars until we are no longer + * overflowing. + */ + if (priv->child) + { + gfloat child_min_width; + gfloat child_min_height; - if (!CLUTTER_ACTOR_IS_VISIBLE (priv->hscroll)) - sb_height = 0; + clutter_actor_get_preferred_width (priv->child, -1, + &child_min_width, NULL); + + if (priv->vscrollbar_policy == GTK_POLICY_AUTOMATIC) + { + if (priv->hscrollbar_policy == GTK_POLICY_AUTOMATIC) + { + /* Pass one, try without a vertical scrollbar */ + clutter_actor_get_preferred_height (priv->child, MAX (avail_width, child_min_width), + &child_min_height, NULL); + vscrollbar_visible = child_min_height > avail_height; + hscrollbar_visible = child_min_width > avail_width - (vscrollbar_visible ? sb_width : 0); + vscrollbar_visible = child_min_height > avail_height - (hscrollbar_visible ? sb_height : 0); + + /* Pass two - if we needed a vertical scrollbar, get a new preferred height */ + if (vscrollbar_visible) + { + clutter_actor_get_preferred_height (priv->child, MAX (avail_width - sb_width, child_min_width), + &child_min_height, NULL); + hscrollbar_visible = child_min_width > avail_width - sb_width; + } + } + else + { + hscrollbar_visible = priv->hscrollbar_policy != GTK_POLICY_NEVER; + + /* try without a vertical scrollbar */ + clutter_actor_get_preferred_height (priv->child, MAX (avail_width, child_min_width), + &child_min_height, NULL); + vscrollbar_visible = child_min_height > avail_height - (hscrollbar_visible ? sb_height : 0); + } + } + else + { + vscrollbar_visible = priv->vscrollbar_policy != GTK_POLICY_NEVER; + + if (priv->hscrollbar_policy == GTK_POLICY_AUTOMATIC) + hscrollbar_visible = child_min_width > avail_height - (vscrollbar_visible ? 0 : sb_width); + else + hscrollbar_visible = priv->hscrollbar_policy != GTK_POLICY_NEVER; + } + } + else + { + hscrollbar_visible = FALSE; + vscrollbar_visible = FALSE; + } + + /* Whether or not we show the scrollbars, if the scrollbars are visible + * actors, we need to give them some allocation, so we unconditionally + * give them the "right" allocation; that might overlap the child when + * the scrollbars are not visible, but it doesn't matter because we + * don't include them in pick or paint. + */ /* Vertical scrollbar */ if (CLUTTER_ACTOR_IS_VISIBLE (priv->vscroll)) @@ -438,6 +641,11 @@ st_scroll_view_allocate (ClutterActor *actor, clutter_actor_allocate (priv->hscroll, &child_box, flags); } + /* Fold visibility into the scrollbar sizes to simplify the rest of the computations */ + if (!hscrollbar_visible) + sb_height = 0; + if (!vscrollbar_visible) + sb_width = 0; /* Child */ child_box.x1 = content_box.x1; @@ -449,23 +657,26 @@ st_scroll_view_allocate (ClutterActor *actor, clutter_actor_allocate (priv->child, &child_box, flags); /*Shadows*/ - if (CLUTTER_ACTOR_IS_VISIBLE (priv->top_shadow)) + if (priv->top_shadow && CLUTTER_ACTOR_IS_VISIBLE (priv->top_shadow)) { child_box.x1 = content_box.x1; child_box.y1 = content_box.y1; child_box.x2 = MAX (child_box.x1, content_box.x2 - sb_width); - child_box.y2 = MAX (child_box.y1, content_box.y1 + clutter_actor_get_height (priv->top_shadow)); + child_box.y2 = content_box.y1 + clutter_actor_get_height (priv->top_shadow); clutter_actor_allocate (priv->top_shadow, &child_box, flags); } - if (CLUTTER_ACTOR_IS_VISIBLE (priv->bottom_shadow)) + if (priv->bottom_shadow && CLUTTER_ACTOR_IS_VISIBLE (priv->bottom_shadow)) { child_box.x1 = content_box.x1; - child_box.y1 = content_box.y2 - clutter_actor_get_height (priv->bottom_shadow); - child_box.x2 = MAX (child_box.x1, content_box.x2 - sb_width); - child_box.y2 = content_box.y2 + 1; + child_box.y1 = MAX (content_box.y1, content_box.y2 - sb_height - clutter_actor_get_height (priv->bottom_shadow)); + child_box.x2 = MAX (content_box.x1, content_box.x2 - sb_width); + child_box.y2 = MAX (content_box.y1, content_box.y2 - sb_height); clutter_actor_allocate (priv->bottom_shadow, &child_box, flags); } + + priv->hscrollbar_visible = hscrollbar_visible; + priv->vscrollbar_visible = vscrollbar_visible; } static void @@ -476,8 +687,11 @@ st_scroll_view_style_changed (StWidget *widget) st_widget_style_changed (ST_WIDGET (priv->hscroll)); st_widget_style_changed (ST_WIDGET (priv->vscroll)); - st_widget_style_changed (ST_WIDGET (priv->top_shadow)); - st_widget_style_changed (ST_WIDGET (priv->bottom_shadow)); + if (priv->top_shadow) + { + st_widget_style_changed (ST_WIDGET (priv->top_shadow)); + st_widget_style_changed (ST_WIDGET (priv->bottom_shadow)); + } ST_WIDGET_CLASS (st_scroll_view_parent_class)->style_changed (widget); } @@ -632,70 +846,67 @@ st_scroll_view_class_init (StScrollViewClass *klass) } static void -child_adjustment_changed_cb (StAdjustment *adjustment, - ClutterActor *bar) +disconnect_hadjustment (StScrollView *scroll) { - StScrollView *scroll; - gdouble lower, upper, page_size; + StScrollViewPrivate *priv = scroll->priv; - scroll = ST_SCROLL_VIEW (clutter_actor_get_parent (bar)); + priv->hadjustment = NULL; +} - if (bar == scroll->priv->vscroll) - update_shadow_visibility (G_OBJECT (adjustment), NULL, scroll); +static void +child_adjustment_changed_cb (StAdjustment *adjustment, + StScrollView *scroll) +{ + update_shadow_visibility (scroll); +} - /* Determine if this scroll-bar should be visible */ - st_adjustment_get_values (adjustment, NULL, - &lower, &upper, - NULL, NULL, - &page_size); +static void +child_adjustment_notify_value (GObject *gobject, + GParamSpec *pspec, + StScrollView *scroll) +{ + update_shadow_visibility (scroll); +} - if ((upper - lower) > page_size) - clutter_actor_show (bar); - else - clutter_actor_hide (bar); +static void +disconnect_vadjustment (StScrollView *scroll) +{ + StScrollViewPrivate *priv = scroll->priv; - /* Request a resize */ - clutter_actor_queue_relayout (CLUTTER_ACTOR (scroll)); + if (priv->vadjustment) + { + g_signal_handlers_disconnect_by_func (priv->vadjustment, + child_adjustment_notify_value, + scroll); + g_signal_handlers_disconnect_by_func (priv->vadjustment, + child_adjustment_changed_cb, + scroll); + priv->vadjustment = NULL; + } } static void -child_hadjustment_notify_cb (GObject *gobject, - GParamSpec *arg1, - gpointer user_data) +child_hadjustment_notify_cb (GObject *gobject, + GParamSpec *arg1, + StScrollView *scroll) { - StAdjustment *hadjust; - ClutterActor *actor = CLUTTER_ACTOR (gobject); - StScrollViewPrivate *priv = ST_SCROLL_VIEW (user_data)->priv; + StScrollViewPrivate *priv = scroll->priv; - hadjust = st_scroll_bar_get_adjustment (ST_SCROLL_BAR(priv->hscroll)); - if (hadjust) - g_signal_handlers_disconnect_by_func (hadjust, - child_adjustment_changed_cb, - priv->hscroll); + disconnect_hadjustment (scroll); - if (priv->hscroll_policy == GTK_POLICY_NEVER) - return; + st_scrollable_get_adjustments (ST_SCROLLABLE (actor), &priv->hadjustment, NULL); + st_scroll_bar_set_adjustment (ST_SCROLL_BAR(priv->hscroll), priv->hadjustment); - st_scrollable_get_adjustments (ST_SCROLLABLE (actor), &hadjust, NULL); - if (hadjust) + if (priv->hadjustment) { /* Force scroll step if neede. */ if (priv->column_size_set) { - g_object_set (hadjust, + g_object_set (priv->hadjustment, "step-increment", priv->column_size, NULL); } - - st_scroll_bar_set_adjustment (ST_SCROLL_BAR(priv->hscroll), hadjust); - - if (priv->hscroll_policy == GTK_POLICY_AUTOMATIC) - { - g_signal_connect (hadjust, "changed", G_CALLBACK ( - child_adjustment_changed_cb), priv->hscroll); - child_adjustment_changed_cb (hadjust, priv->hscroll); - } } } @@ -704,44 +915,29 @@ child_vadjustment_notify_cb (GObject *gobject, GParamSpec *arg1, gpointer user_data) { - StAdjustment *vadjust; + StScrollView *scroll = ST_SCROLL_VIEW (user_data); + StScrollViewPrivate *priv = scroll->priv; - ClutterActor *actor = CLUTTER_ACTOR (gobject); - StScrollViewPrivate *priv = ST_SCROLL_VIEW (user_data)->priv; + disconnect_vadjustment (scroll); - vadjust = st_scroll_bar_get_adjustment (ST_SCROLL_BAR(priv->vscroll)); - if (vadjust) - { - g_signal_handlers_disconnect_by_func (vadjust, - update_shadow_visibility, - user_data); - g_signal_handlers_disconnect_by_func (vadjust, - child_adjustment_changed_cb, - priv->vscroll); - } - if (priv->vscroll_policy == GTK_POLICY_NEVER) - return; - st_scrollable_get_adjustments (ST_SCROLLABLE(actor), NULL, &vadjust); - if (vadjust) + st_scrollable_get_adjustments (ST_SCROLLABLE(priv->child), NULL, &priv->vadjustment); + st_scroll_bar_set_adjustment (ST_SCROLL_BAR(priv->vscroll), priv->vadjustment); + + if (priv->vadjustment) { /* Force scroll step if neede. */ if (priv->row_size_set) { - g_object_set (vadjust, + g_object_set (priv->vadjustment, "step-increment", priv->row_size, NULL); } - st_scroll_bar_set_adjustment (ST_SCROLL_BAR(priv->vscroll), vadjust); - if (priv->vscroll_policy == GTK_POLICY_AUTOMATIC) - { - g_signal_connect (vadjust, "changed", G_CALLBACK ( - child_adjustment_changed_cb), priv->vscroll); - g_signal_connect (vadjust, "notify::value", G_CALLBACK ( - update_shadow_visibility), user_data); - child_adjustment_changed_cb (vadjust, priv->vscroll); - update_shadow_visibility (G_OBJECT (vadjust), NULL, user_data); - } + g_signal_connect (priv->vadjustment, "changed", + G_CALLBACK (child_adjustment_changed_cb), scroll); + g_signal_connect (priv->vadjustment, "notify::value", + G_CALLBACK (child_adjustment_notify_value), scroll); + update_shadow_visibility (scroll); } } @@ -750,8 +946,8 @@ st_scroll_view_init (StScrollView *self) { StScrollViewPrivate *priv = self->priv = SCROLL_VIEW_PRIVATE (self); - priv->hscroll_policy = GTK_POLICY_AUTOMATIC; - priv->vscroll_policy = GTK_POLICY_AUTOMATIC; + priv->hscrollbar_policy = GTK_POLICY_AUTOMATIC; + priv->vscrollbar_policy = GTK_POLICY_AUTOMATIC; priv->hscroll = CLUTTER_ACTOR (st_scroll_bar_new (NULL)); priv->vscroll = g_object_new (ST_TYPE_SCROLL_BAR, "vertical", TRUE, NULL); @@ -759,12 +955,6 @@ st_scroll_view_init (StScrollView *self) clutter_actor_set_parent (priv->hscroll, CLUTTER_ACTOR (self)); clutter_actor_set_parent (priv->vscroll, CLUTTER_ACTOR (self)); - priv->top_shadow = g_object_new (ST_TYPE_BIN, "style-class", "top-shadow", NULL); - priv->bottom_shadow = g_object_new (ST_TYPE_BIN, "style-class", "bottom-shadow", NULL); - - clutter_actor_set_parent (priv->bottom_shadow, CLUTTER_ACTOR (self)); - clutter_actor_set_parent (priv->top_shadow, CLUTTER_ACTOR (self)); - /* mouse scroll is enabled by default, so we also need to be reactive */ priv->mouse_scroll = TRUE; g_object_set (G_OBJECT (self), "reactive", TRUE, NULL); @@ -791,8 +981,8 @@ st_scroll_view_add (ClutterContainer *container, g_signal_connect (actor, "notify::vadjustment", G_CALLBACK (child_vadjustment_notify_cb), container); - child_hadjustment_notify_cb (G_OBJECT (actor), NULL, container); - child_vadjustment_notify_cb (G_OBJECT (actor), NULL, container); + child_hadjustment_notify_cb (G_OBJECT (actor), NULL, self); + child_vadjustment_notify_cb (G_OBJECT (actor), NULL, self); } else { @@ -856,8 +1046,11 @@ st_scroll_view_foreach_with_internals (ClutterContainer *container, if (priv->vscroll != NULL) callback (priv->vscroll, user_data); - callback (priv->top_shadow, user_data); - callback (priv->bottom_shadow, user_data); + if (priv->top_shadow) + { + callback (priv->top_shadow, user_data); + callback (priv->bottom_shadow, user_data); + } } static void @@ -1048,58 +1241,29 @@ st_scroll_view_set_policy (StScrollView *scroll, GtkPolicyType vscroll) { StScrollViewPrivate *priv; - StAdjustment *hadjust, *vadjust; g_return_if_fail (ST_IS_SCROLL_VIEW (scroll)); - g_return_if_fail (hscroll != GTK_POLICY_ALWAYS && vscroll != GTK_POLICY_ALWAYS); priv = ST_SCROLL_VIEW (scroll)->priv; - if (priv->hscroll_policy == hscroll && priv->vscroll_policy == vscroll) + if (priv->hscrollbar_policy == hscroll && priv->vscrollbar_policy == vscroll) return; g_object_freeze_notify ((GObject *) scroll); - if (priv->hscroll_policy != hscroll) + if (priv->hscrollbar_policy != hscroll) { - priv->hscroll_policy = hscroll; + priv->hscrollbar_policy = hscroll; g_object_notify ((GObject *) scroll, "hscrollbar-policy"); } - if (priv->vscroll_policy != vscroll) - { - priv->vscroll_policy = vscroll; - g_object_notify ((GObject *) scroll, "vscrollbar-policy"); - } - - st_scrollable_get_adjustments (ST_SCROLLABLE (priv->child), &hadjust, &vadjust); - if (priv->hscroll_policy == GTK_POLICY_NEVER) - { - hadjust = NULL; - clutter_actor_hide (priv->hscroll); - } - else if (priv->hscroll_policy == GTK_POLICY_AUTOMATIC) + if (priv->vscrollbar_policy != vscroll) { - /* We call this function because we need to set up the adjustment - * notification hooks, and the _show will queue a reallocate, from which we'll figure - * out if we need to really show or hide the scrollbar. - */ - child_hadjustment_notify_cb (G_OBJECT (priv->child), NULL, scroll); - clutter_actor_show (priv->hscroll); - } - - if (priv->vscroll_policy == GTK_POLICY_NEVER) - { - vadjust = NULL; - clutter_actor_hide (priv->vscroll); - } - else - { - child_vadjustment_notify_cb (G_OBJECT (priv->child), NULL, scroll); - clutter_actor_show (priv->vscroll); + priv->vscrollbar_policy = vscroll; + g_object_notify ((GObject *) scroll, "vscrollbar-policy"); } - st_scrollable_set_adjustments (ST_SCROLLABLE (priv->child), hadjust, vadjust); + clutter_actor_queue_relayout (CLUTTER_ACTOR (scroll)); g_object_thaw_notify ((GObject *) scroll); } diff --git a/src/st/st-scrollable.c b/src/st/st-scrollable.c index acf5ec584..38a25b199 100644 --- a/src/st/st-scrollable.c +++ b/src/st/st-scrollable.c @@ -25,6 +25,50 @@ #include "st-scrollable.h" +/** + * SECTION:st-scrollable + * @short_description: A #ClutterActor that can be scrolled + * + * The #StScrollable interface is exposed by actors that support scrolling. + * + * The interface contains methods for getting and setting the adjustments + * for scrolling; these adjustments will be used to hook the scrolled + * position up to scrollbars or other external controls. + * + * For #StScrollable supporting height-for-width size negotation, size + * negotation works as follows: + * + * In response to get_preferred_width(), the scrollable should report + * the minimum width at which horizontal scrolling is needed for the + * preferred width, and natural width of the actor when not + * horizontally scrolled as the natural width. + * + * The for_width passed into get_preferred_height() is the width at which + * the scrollable will be allocated; this will be smaller than the minimum + * width when scrolling horizontally, so the scrollable may want to adjust + * it up to the minimum width before computing a preferred height. (Other + * scrollables may want to fit as much content into the allocated area + * as possible and only scroll what absolutely needs to scroll - consider, + * for example, the line-wrapping behavior of a text editor where there + * is a long line without any spaces.) As for width, get_preferred_height() + * should return the minimum size at which no scrolling is needed for the + * minimum height, and the natural size of the actor when not vertically scrolled + * as the natural height. + * + * In allocate() the allocation box passed in will be actual allocated + * size of the actor so will be smaller than the reported minimum + * width and/or height when scrolling is present. Any scrollable actor + * must support being allocated at any size down to 0x0 without + * crashing, however if the actor has content around the scrolled area + * and has an absolute minimum size that's bigger than 0x0 its + * acceptable for it to misdraw between 0x0 and the absolute minimum + * size. It's up to the application author to avoid letting the user + * resize the scroll view small enough so that the scrolled area + * vanishes. + * + * width-for-height size negotation is similar, with the roles of width + * and height reversed. + */ static void st_scrollable_base_init (gpointer g_iface) { diff --git a/tests/interactive/scroll-view-sizing.js b/tests/interactive/scroll-view-sizing.js new file mode 100644 index 000000000..c7c8c9541 --- /dev/null +++ b/tests/interactive/scroll-view-sizing.js @@ -0,0 +1,348 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const Clutter = imports.gi.Clutter; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; +const Shell = imports.gi.Shell; +const St = imports.gi.St; + +const UI = imports.testcommon.ui; + +// This is an interactive test of the sizing behavior of StScrollView. It +// may be interesting in the future to split out the two classes at the +// top into utility classes for testing the sizing behavior of other +// containers and actors. + +/****************************************************************************/ + +// FlowedBoxes: This is a simple actor that demonstrates an interesting +// height-for-width behavior. A set of boxes of different sizes are line-wrapped +// horizontally with the minimum horizontal size being determined by the +// largest box. It would be easy to extend this to allow doing vertical +// wrapping instead, if you wanted to see just how badly our width-for-height +// implementation is or work on fixing it. + +const BOX_HEIGHT = 20; +const BOX_WIDTHS = [ + 10, 40, 100, 20, 60, 30, 70, 10, 20, 200, 50, 70, 90, 20, 40 +]; + +const SPACING = 10; + +function FlowedBoxes() { + this._init(); +} + +FlowedBoxes.prototype = { + _init: function() { + this.actor = new Shell.GenericContainer(); + this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth)); + this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight)); + this.actor.connect('allocate', Lang.bind(this, this._allocate)); + + for (let i = 0; i < BOX_WIDTHS.length; i++) { + let child = new St.Bin({ width: BOX_WIDTHS[i], height: BOX_HEIGHT, + style: 'border: 1px solid #444444; background: #00aa44' }) + this.actor.add_actor(child); + } + }, + + _getPreferredWidth: function (actor, forHeight, alloc) { + let children = this.actor.get_children(); + + let maxMinWidth = 0; + let totalNaturalWidth = 0; + + for (let i = 0; i < children.length; i++) { + let child = children[i]; + let [minWidth, naturalWidth] = child.get_preferred_width(-1); + maxMinWidth = Math.max(maxMinWidth, minWidth); + if (i != 0) + totalNaturalWidth += SPACING; + totalNaturalWidth += naturalWidth; + } + + alloc.min_size = maxMinWidth; + alloc.natural_size = totalNaturalWidth; + }, + + _layoutChildren: function(forWidth, callback) { + let children = this.actor.get_children(); + + let x = 0; + let y = 0; + for (let i = 0; i < children.length; i++) { + let child = children[i]; + let [minWidth, naturalWidth] = child.get_preferred_width(-1); + let [minHeight, naturalHeight] = child.get_preferred_height(naturalWidth); + + let x1 = x; + if (x != 0) + x1 += SPACING; + let x2 = x1 + naturalWidth; + + if (x2 > forWidth) { + if (x > 0) { + x1 = 0; + y += BOX_HEIGHT + SPACING; + } + + x2 = naturalWidth; + } + + callback(child, x1, y, x2, y + naturalHeight); + x = x2; + } + + }, + + _getPreferredHeight: function (actor, forWidth, alloc) { + let height = 0; + this._layoutChildren(forWidth, + function(child, x1, y1, x2, y2) { + height = Math.max(height, y2); + }); + + alloc.min_size = alloc.natural_size = height; + }, + + _allocate: function (actor, box, flags) { + this._layoutChildren(box.x2 - box.x1, + function(child, x1, y1, x2, y2) { + child.allocate(new Clutter.ActorBox({ x1: x1, y1: y1, x2: x2, y2: y2 }), + flags); + }); + } +}; + +/****************************************************************************/ + +// SizingIllustrator: this is a container that allows interactively exploring +// the sizing behavior of the child. Lines are drawn to indicate the minimum +// and natural size of the child, and a drag handle allows the user to resize +// the child interactively and see how that affects it. +// +// This is currently only written for the case where the child is height-for-width + +function SizingIllustrator() { + this._init(); +} + +SizingIllustrator.prototype = { + _init: function() { + this.actor = new Shell.GenericContainer(); + + this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth)); + this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight)); + this.actor.connect('allocate', Lang.bind(this, this._allocate)); + + this.minWidthLine = new St.Bin({ style: 'background: red' }); + this.actor.add_actor(this.minWidthLine); + this.minHeightLine = new St.Bin({ style: 'background: red' }); + this.actor.add_actor(this.minHeightLine); + + this.naturalWidthLine = new St.Bin({ style: 'background: #4444ff' }); + this.actor.add_actor(this.naturalWidthLine); + this.naturalHeightLine = new St.Bin({ style: 'background: #4444ff' }); + this.actor.add_actor(this.naturalHeightLine); + + this.currentWidthLine = new St.Bin({ style: 'background: #aaaaaa' }); + this.actor.add_actor(this.currentWidthLine); + this.currentHeightLine = new St.Bin({ style: 'background: #aaaaaa' }); + this.actor.add_actor(this.currentHeightLine); + + this.handle = new St.Bin({ style: 'background: yellow; border: 1px solid black;', + reactive: true }); + this.handle.connect('button-press-event', Lang.bind(this, this._handlePressed)); + this.handle.connect('button-release-event', Lang.bind(this, this._handleReleased)); + this.handle.connect('motion-event', Lang.bind(this, this._handleMotion)); + this.actor.add_actor(this.handle); + + this._inDrag = false; + + this.width = 300; + this.height = 300; + }, + + add: function(child) { + this.child = child; + this.actor.add_actor(this.child); + this.child.lower_bottom(); + }, + + _getPreferredWidth: function (actor, forHeight, alloc) { + let children = this.actor.get_children(); + for (let i = 0; i < children.length; i++) { + let child = children[i]; + let [minWidth, naturalWidth] = child.get_preferred_width(-1); + if (child == this.child) { + this.minWidth = minWidth; + this.naturalWidth = naturalWidth; + } + } + + alloc.min_size = 0; + alloc.natural_size = 400; + }, + + _getPreferredHeight: function (actor, forWidth, alloc) { + let children = this.actor.get_children(); + for (let i = 0; i < children.length; i++) { + let child = children[i]; + if (child == this.child) { + [this.minHeight, this.naturalHeight] = child.get_preferred_height(this.width); + } else { + let [minWidth, naturalWidth] = child.get_preferred_width(-1); + child.get_preferred_height(naturalWidth); + } + } + + alloc.min_size = 0; + alloc.natural_size = 400; + }, + + _allocate: function (actor, box, flags) { + let allocWidth = box.x2 - box.x1; + let allocHeight = box.y2 - box.y1; + + function alloc(child, x1, y1, x2, y2) { + child.allocate(new Clutter.ActorBox({ x1: x1, y1: y1, x2: x2, y2: y2 }), + flags); + } + + alloc(this.child, 0, 0, this.width, this.height); + alloc(this.minWidthLine, this.minWidth, 0, this.minWidth + 1, allocHeight); + alloc(this.naturalWidthLine, this.naturalWidth, 0, this.naturalWidth + 1, allocHeight); + alloc(this.currentWidthLine, this.width, 0, this.width + 1, allocHeight); + alloc(this.minHeightLine, 0, this.minHeight, allocWidth, this.minHeight + 1); + alloc(this.naturalHeightLine, 0, this.naturalHeight, allocWidth, this.naturalHeight + 1); + alloc(this.currentHeightLine, 0, this.height, allocWidth, this.height + 1); + alloc(this.handle, this.width, this.height, this.width + 10, this.height + 10); + }, + + _handlePressed: function(handle, event) { + if (event.get_button() == 1) { + this._inDrag = true; + let [handleX, handleY] = handle.get_transformed_position(); + let [x, y] = event.get_coords(); + this._dragX = x - handleX; + this._dragY = y - handleY; + Clutter.grab_pointer(handle); + } + }, + + _handleReleased: function(handle, event) { + if (event.get_button() == 1) { + this._inDrag = false; + Clutter.ungrab_pointer(handle); + } + }, + + _handleMotion: function(handle, event) { + if (this._inDrag) { + let [x, y] = event.get_coords(); + let [actorX, actorY] = this.actor.get_transformed_position(); + this.width = x - this._dragX - actorX; + this.height = y - this._dragY - actorY; + this.actor.queue_relayout(); + } + } +}; + +/****************************************************************************/ + +UI.init(); +let stage = Clutter.Stage.get_default(); +stage.width = 600; +stage.height = 600; + +let mainBox = new St.BoxLayout({ width: stage.width, + height: stage.height, + vertical: true, + style: 'padding: 10px;' + + 'spacing: 5px;' + + 'font: 16px sans-serif;' + + 'background: black;' + + 'color: white;' }); +stage.add_actor(mainBox); + +const DOCS = 'Red lines represent minimum size, blue lines natural size. Drag yellow handle to resize ScrollView. Click on options to change.'; + +let docsLabel = new St.Label({ text: DOCS }); +docsLabel.clutter_text.line_wrap = true; +mainBox.add(docsLabel); + +let bin = new St.Bin({ x_fill: true, y_fill: true, style: 'border: 2px solid #666666;' }); +mainBox.add(bin, { x_fill: true, y_fill: true, expand: true }); + +let illustrator = new SizingIllustrator(); +bin.add_actor(illustrator.actor); + +let scrollView = new St.ScrollView(); +illustrator.add(scrollView); + +let box = new St.BoxLayout({ vertical: true }); +scrollView.add_actor(box); + +let flowedBoxes = new FlowedBoxes(); +box.add(flowedBoxes.actor, { expand: false, x_fill: true, y_fill: true }); + +let policyBox = new St.BoxLayout({ vertical: false }); +mainBox.add(policyBox); + +policyBox.add(new St.Label({ text: 'Horizontal Policy: ' })); +let hpolicy = new St.Button({ label: 'AUTOMATIC', style: 'text-decoration: underline; color: #4444ff;' }); +policyBox.add(hpolicy); + +let spacer = new St.Bin(); +policyBox.add(spacer, { expand: true }); + +policyBox.add(new St.Label({ text: 'Vertical Policy: '})); +let vpolicy = new St.Button({ label: 'AUTOMATIC', style: 'text-decoration: underline; color: #4444ff;' }); +policyBox.add(vpolicy); + +function togglePolicy(button) { + switch(button.label) { + case 'AUTOMATIC': + button.label = 'ALWAYS'; + break; + case 'ALWAYS': + button.label = 'NEVER'; + break; + case 'NEVER': + button.label = 'AUTOMATIC'; + break; + } + scrollView.set_policy(Gtk.PolicyType[hpolicy.label], Gtk.PolicyType[vpolicy.label]); +} + +hpolicy.connect('clicked', function() { togglePolicy(hpolicy); }); +vpolicy.connect('clicked', function() { togglePolicy(vpolicy); }); + +let shadowsBox = new St.BoxLayout({ vertical: false }); +mainBox.add(shadowsBox); + +spacer = new St.Bin(); +shadowsBox.add(spacer, { expand: true }); + +shadowsBox.add(new St.Label({ text: 'Vertical Shadows: '})); +let vshadows = new St.Button({ label: 'No', style: 'text-decoration: underline; color: #4444ff;' }); +shadowsBox.add(vshadows); + +function toggleShadows(button) { + switch(button.label) { + case 'No': + button.label = 'Yes'; + break; + case 'Yes': + button.label = 'No'; + break; + } + scrollView.set_vshadows(vshadows.label == 'Yes'); +} + +vshadows.connect('clicked', function() { toggleShadows(vshadows); }); + +stage.show(); +Clutter.main(); +stage.destroy();